;(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? (module.exports = factory())
    : typeof define === 'function' && define.amd
    ? define(factory)
    : ((global =
        typeof globalThis !== 'undefined' ? globalThis : global || self),
      (global.Pristine = factory()))
})(this, function () {
  'use strict'

  var lang = {
    en: {
      required: 'This field is required',
      email: 'This field requires a valid e-mail address',
      number: 'This field requires a number',
      integer: 'This field requires an integer value',
      url: 'This field requires a valid website URL',
      tel: 'This field requires a valid telephone number',
      maxlength: 'This fields length must be < ${1}',
      minlength: 'This fields length must be > ${1}',
      min: 'Minimum value for this field is ${1}',
      max: 'Maximum value for this field is ${1}',
      pattern: 'Please match the requested format',
      equals: 'The two fields do not match',
    },
  }

  function findAncestor(el, cls) {
    while ((el = el.parentElement) && !el.classList.contains(cls)) {}
    return el
  }

  function tmpl(o) {
    var _arguments = arguments

    return this.replace(/\${([^{}]*)}/g, function (a, b) {
      return _arguments[b]
    })
  }

  function groupedElemCount(input) {
    return input.pristine.self.form.querySelectorAll(
      'input[name="' + input.getAttribute('name') + '"]:checked'
    ).length
  }

  function mergeConfig(obj1, obj2) {
    for (var attr in obj2) {
      if (!(attr in obj1)) {
        obj1[attr] = obj2[attr]
      }
    }
    return obj1
  }

  var defaultConfig = {
    classTo: 'form-group',
    errorClass: 'has-danger',
    successClass: 'has-success',
    errorTextParent: 'form-group',
    errorTextTag: 'div',
    errorTextClass: 'text-help',
  }

  var PRISTINE_ERROR = 'pristine-error'
  var SELECTOR =
    'input:not([type^=hidden]):not([type^=submit]):not([type^=button]):not(.skip-validation), select, textarea'
  var ALLOWED_ATTRIBUTES = [
    'required',
    'min',
    'max',
    'minlength',
    'maxlength',
    'pattern',
  ]
  var EMAIL_REGEX =
    /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

  var MESSAGE_REGEX = /-message(?:-([a-z]{2}(?:_[A-Z]{2})?))?/ // matches, -message, -message-en, -message-en_US
  var currentLocale = 'en'
  var validators = {}

  var _ = function _(name, validator) {
    validator.name = name
    if (validator.priority === undefined) validator.priority = 1
    validators[name] = validator
  }

  _('text', {
    fn: function fn(val) {
      return true
    },
    priority: 0,
  })
  _('required', {
    fn: function fn(val) {
      return this.type === 'radio' || this.type === 'checkbox'
        ? groupedElemCount(this)
        : val !== undefined && val !== ''
    },
    priority: 99,
    halt: true,
  })
  _('email', {
    fn: function fn(val) {
      return !val || EMAIL_REGEX.test(val)
    },
  })
  _('number', {
    fn: function fn(val) {
      return !val || !isNaN(parseFloat(val))
    },
    priority: 2,
  })
  _('integer', {
    fn: function fn(val) {
      return !val || /^\d+$/.test(val)
    },
  })
  _('minlength', {
    fn: function fn(val, length) {
      return !val || val.length >= parseInt(length)
    },
  })
  _('maxlength', {
    fn: function fn(val, length) {
      return !val || val.length <= parseInt(length)
    },
  })
  _('min', {
    fn: function fn(val, limit) {
      return (
        !val ||
        (this.type === 'checkbox'
          ? groupedElemCount(this) >= parseInt(limit)
          : parseFloat(val) >= parseFloat(limit))
      )
    },
  })
  _('max', {
    fn: function fn(val, limit) {
      return (
        !val ||
        (this.type === 'checkbox'
          ? groupedElemCount(this) <= parseInt(limit)
          : parseFloat(val) <= parseFloat(limit))
      )
    },
  })
  _('pattern', {
    fn: function fn(val, pattern) {
      var m = pattern.match(new RegExp('^/(.*?)/([gimy]*)$'))
      return !val || new RegExp(m[1], m[2]).test(val)
    },
  })
  _('equals', {
    fn: function fn(val, otherFieldSelector) {
      var other = document.querySelector(otherFieldSelector)
      return other && ((!val && !other.value) || other.value === val)
    },
  })

  function Pristine(form, config, live) {
    var self = this

    init(form, config, live)

    function init(form, config, live) {
      form.setAttribute('novalidate', 'true')

      self.form = form
      self.config = mergeConfig(config || {}, defaultConfig)
      self.live = !(live === false)
      self.fields = Array.from(form.querySelectorAll(SELECTOR)).map(
        function (input) {
          var fns = []
          var params = {}
          var messages = {}

          ;[].forEach.call(input.attributes, function (attr) {
            if (/^data-pristine-/.test(attr.name)) {
              var name = attr.name.substr(14)
              var messageMatch = name.match(MESSAGE_REGEX)
              if (messageMatch !== null) {
                var locale =
                  messageMatch[1] === undefined ? 'en' : messageMatch[1]
                if (!messages.hasOwnProperty(locale)) messages[locale] = {}
                messages[locale][
                  name.slice(0, name.length - messageMatch[0].length)
                ] = attr.value
                return
              }
              if (name === 'type') name = attr.value
              _addValidatorToField(fns, params, name, attr.value)
            } else if (~ALLOWED_ATTRIBUTES.indexOf(attr.name)) {
              _addValidatorToField(fns, params, attr.name, attr.value)
            } else if (attr.name === 'type') {
              _addValidatorToField(fns, params, attr.value)
            }
          })

          fns.sort(function (a, b) {
            return b.priority - a.priority
          })

          self.live &&
            input.addEventListener(
              !~['radio', 'checkbox'].indexOf(input.getAttribute('type'))
                ? 'input'
                : 'change',
              function (e) {
                self.validate(e.target)
              }.bind(self)
            )

          return (input.pristine = {
            input: input,
            validators: fns,
            params: params,
            messages: messages,
            self: self,
          })
        }.bind(self)
      )
    }

    function _addValidatorToField(fns, params, name, value) {
      var validator = validators[name]
      if (validator) {
        fns.push(validator)
        if (value) {
          var valueParams = name === 'pattern' ? [value] : value.split(',')
          valueParams.unshift(null) // placeholder for input's value
          params[name] = valueParams
        }
      }
    }

    /***
     * Checks whether the form/input elements are valid
     * @param input => input element(s) or a jquery selector, null for full form validation
     * @param silent => do not show error messages, just return true/false
     * @returns {boolean} return true when valid false otherwise
     */
    self.validate = function (input, silent) {
      silent = (input && silent === true) || input === true
      var fields = self.fields
      if (input !== true && input !== false) {
        if (input instanceof HTMLElement) {
          fields = [input.pristine]
        } else if (
          input instanceof NodeList ||
          input instanceof (window.$ || Array) ||
          input instanceof Array
        ) {
          fields = Array.from(input).map(function (el) {
            return el.pristine
          })
        }
      }

      var valid = true

      for (var i = 0; fields[i]; i++) {
        var field = fields[i]
        if (_validateField(field)) {
          !silent && _showSuccess(field)
        } else {
          valid = false
          !silent && _showError(field)
        }
      }
      return valid
    }

    /***
     * Get errors of a specific field or the whole form
     * @param input
     * @returns {Array|*}
     */
    self.getErrors = function (input) {
      if (!input) {
        var erroneousFields = []
        for (var i = 0; i < self.fields.length; i++) {
          var field = self.fields[i]
          if (field.errors.length) {
            erroneousFields.push({ input: field.input, errors: field.errors })
          }
        }
        return erroneousFields
      }
      if (input.tagName && input.tagName.toLowerCase() === 'select') {
        return input.pristine.errors
      }
      return input.length ? input[0].pristine.errors : input.pristine.errors
    }

    /***
     * Validates a single field, all validator functions are called and error messages are generated
     * when a validator fails
     * @param field
     * @returns {boolean}
     * @private
     */
    function _validateField(field) {
      var errors = []
      var valid = true
      for (var i = 0; field.validators[i]; i++) {
        var validator = field.validators[i]
        var params = field.params[validator.name]
          ? field.params[validator.name]
          : []
        params[0] = field.input.value
        if (!validator.fn.apply(field.input, params)) {
          valid = false

          if (typeof validator.msg === 'function') {
            errors.push(validator.msg(field.input.value, params))
          } else if (typeof validator.msg === 'string') {
            errors.push(tmpl.apply(validator.msg, params))
          } else if (
            validator.msg === Object(validator.msg) &&
            validator.msg[currentLocale]
          ) {
            // typeof generates unnecessary babel code
            errors.push(tmpl.apply(validator.msg[currentLocale], params))
          } else if (
            field.messages[currentLocale] &&
            field.messages[currentLocale][validator.name]
          ) {
            errors.push(
              tmpl.apply(field.messages[currentLocale][validator.name], params)
            )
          } else if (
            lang[currentLocale] &&
            lang[currentLocale][validator.name]
          ) {
            errors.push(tmpl.apply(lang[currentLocale][validator.name], params))
          }

          if (validator.halt === true) {
            break
          }
        }
      }
      field.errors = errors
      return valid
    }

    /***
     * Add a validator to a specific dom element in a form
     * @param elem => The dom element where the validator is applied to
     * @param fn => validator function
     * @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
     * so on are for the attribute values
     * @param priority => priority of the validator function, higher valued function gets called first.
     * @param halt => whether validation should stop for this field after current validation function
     */
    self.addValidator = function (elem, fn, msg, priority, halt) {
      if (elem instanceof HTMLElement) {
        elem.pristine.validators.push({
          fn: fn,
          msg: msg,
          priority: priority,
          halt: halt,
        })
        elem.pristine.validators.sort(function (a, b) {
          return b.priority - a.priority
        })
      } else {
        console.warn('The parameter elem must be a dom element')
      }
    }

    /***
     * An utility function that returns a 2-element array, first one is the element where error/success class is
     * applied. 2nd one is the element where error message is displayed. 2nd element is created if doesn't exist and cached.
     * @param field
     * @returns {*}
     * @private
     */
    function _getErrorElements(field) {
      if (field.errorElements) {
        return field.errorElements
      }
      var errorClassElement = findAncestor(field.input, self.config.classTo)
      var errorTextParent = null,
        errorTextElement = null
      if (self.config.classTo === self.config.errorTextParent) {
        errorTextParent = errorClassElement
      } else {
        errorTextParent = errorClassElement.querySelector(
          '.' + self.config.errorTextParent
        )
      }
      if (errorTextParent) {
        errorTextElement = errorTextParent.querySelector('.' + PRISTINE_ERROR)
        if (!errorTextElement) {
          errorTextElement = document.createElement(self.config.errorTextTag)
          errorTextElement.className =
            PRISTINE_ERROR + ' ' + self.config.errorTextClass
          errorTextParent.appendChild(errorTextElement)
          errorTextElement.pristineDisplay = errorTextElement.style.display
        }
      }
      return (field.errorElements = [errorClassElement, errorTextElement])
    }

    function _showError(field) {
      var errorElements = _getErrorElements(field)
      var errorClassElement = errorElements[0],
        errorTextElement = errorElements[1]

      if (errorClassElement) {
        errorClassElement.classList.remove(self.config.successClass)
        errorClassElement.classList.add(self.config.errorClass)
      }
      if (errorTextElement) {
        errorTextElement.innerHTML = field.errors.join('<br/>')
        errorTextElement.style.display = errorTextElement.pristineDisplay || ''
      }
    }

    /***
     * Adds error to a specific field
     * @param input
     * @param error
     */
    self.addError = function (input, error) {
      input = input.length ? input[0] : input
      input.pristine.errors.push(error)
      _showError(input.pristine)
    }

    function _removeError(field) {
      var errorElements = _getErrorElements(field)
      var errorClassElement = errorElements[0],
        errorTextElement = errorElements[1]
      if (errorClassElement) {
        // IE > 9 doesn't support multiple class removal
        errorClassElement.classList.remove(self.config.errorClass)
        errorClassElement.classList.remove(self.config.successClass)
      }
      if (errorTextElement) {
        errorTextElement.innerHTML = ''
        errorTextElement.style.display = 'none'
      }
      return errorElements
    }

    function _showSuccess(field) {
      var errorClassElement = _removeError(field)[0]
      errorClassElement &&
        errorClassElement.classList.add(self.config.successClass)
    }

    /***
     * Resets the errors
     */
    self.reset = function () {
      for (var i = 0; self.fields[i]; i++) {
        self.fields[i].errorElements = null
      }
      Array.from(self.form.querySelectorAll('.' + PRISTINE_ERROR)).map(
        function (elem) {
          elem.parentNode.removeChild(elem)
        }
      )
      Array.from(self.form.querySelectorAll('.' + self.config.classTo)).map(
        function (elem) {
          elem.classList.remove(self.config.successClass)
          elem.classList.remove(self.config.errorClass)
        }
      )
    }

    /***
     * Resets the errors and deletes all pristine fields
     */
    self.destroy = function () {
      self.reset()
      self.fields.forEach(function (field) {
        delete field.input.pristine
      })
      self.fields = []
    }

    self.setGlobalConfig = function (config) {
      defaultConfig = config
    }

    return self
  }

  /***
   *
   * @param name => Name of the global validator
   * @param fn => validator function
   * @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
   * so on are for the attribute values
   * @param priority => priority of the validator function, higher valued function gets called first.
   * @param halt => whether validation should stop for this field after current validation function
   */
  Pristine.addValidator = function (name, fn, msg, priority, halt) {
    _(name, { fn: fn, msg: msg, priority: priority, halt: halt })
  }

  Pristine.addMessages = function (locale, messages) {
    var langObj = lang.hasOwnProperty(locale)
      ? lang[locale]
      : (lang[locale] = {})

    Object.keys(messages).forEach(function (key, index) {
      langObj[key] = messages[key]
    })
  }

  Pristine.setLocale = function (locale) {
    currentLocale = locale
  }

  return Pristine
})
